using System;
using System.Text;
using System.Net;
using System.Security.Cryptography;
using System.Globalization;

namespace Rei.Net
{
    /// <summary>
    /// WSSE Authentication Module
    /// Custom authorization module of WSSE, which most of AtomAPI services use for authorization method. The module implements IAuthenticationModule interfaces and therefore it is compatible with WebRequest/Response authentication layer.
    /// http://www.codeproject.com/KB/IP/WSSEClient.aspx
    /// </summary>
    class WSSEClient : IAuthenticationModule
    {
        internal const string AuthType = "WSSE";
        internal const string Signature = "WSSE";
        internal const int NonceLength = 0x10;
        private readonly RNGCryptoServiceProvider Rand;

        /// <summary>
        /// Initialization constructor
        /// </summary>
        public WSSEClient()
        {
            //initialize random generator
            Rand = new RNGCryptoServiceProvider();
        }

        /// <summary>
        /// Name of authenticationtype.
        /// </summary>
        public string AuthenticationType
        {
            get
            {
                return AuthType;
            }
        }

        /// <summary>
        /// True means that the module supports pre-authentication. 
        /// WSSE authentication can support pre-authentication. 
        /// </summary>
        public bool CanPreAuthenticate
        {
            get
            {
                return true;
            }
        }


        /// <summary>
        /// called by AuthenticationManager when the WebRequest needs authentication information. The implementations of these functions are almost the same. The only difference is that PreAuthenticate function doesn't have challenge argument because it calls before sending a request. 
        /// </summary>
        /// <param name="challenge"></param>
        /// <param name="request"></param>
        /// <param name="credentials"></param>
        /// <returns></returns>
        public Authorization Authenticate(string challenge, WebRequest request, ICredentials credentials)
        {
            if (!ContainsSignatureInChallenge(challenge, Signature)) return null;
            return this.InnerAuthenticate(request, credentials);
        }

        /// <summary>
        /// called by AuthenticationManager when the WebRequest needs authentication information. The implementations of these functions are almost the same. The only difference is that PreAuthenticate function doesn't have challenge argument because it calls before sending a request. 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="credentials"></param>
        /// <returns></returns>
        public Authorization PreAuthenticate(WebRequest request, ICredentials credentials)
        {
            return this.InnerAuthenticate(request, credentials);
        }

        /// <summary>
        /// Authentication Core
        /// </summary>
        private Authorization InnerAuthenticate(WebRequest request, ICredentials credentials)
        {
            try
            {
                //get credential
                if (credentials == null) return null;
                NetworkCredential credential = credentials.GetCredential(request.RequestUri, AuthenticationType);
                if (credential == null) return null;
                
                //check credential policy
                //request.RequestURI is not actually challenged uri. but I don't know how I can get it...
                ICredentialPolicy credentialpolicy = AuthenticationManager.CredentialPolicy;
                if ((credentialpolicy != null) && (!credentialpolicy.ShouldSendCredential(request.RequestUri, request, credential, this))) return null;
                
                //make secure nonce
                byte[] nonce = CreateNonce();

                //make iso8601 formatted UTC time
                string createtime = DateTime.Now.ToUniversalTime().ToString(DateTimeFormatInfo.InvariantInfo.SortableDateTimePattern) + "Z";

                //make buffer. createtime and password are encoded in UTF8
                byte[] digestbuf = new byte[nonce.Length + Encoding.UTF8.GetByteCount(createtime + credential.Password)];
                nonce.CopyTo(digestbuf, 0);
                Encoding.UTF8.GetBytes(createtime + credential.Password).CopyTo(digestbuf, nonce.Length);

                //create default SHA1
                SHA1 sha1 = SHA1.Create();

                //make digest string
                string digest = Convert.ToBase64String(sha1.ComputeHash(digestbuf));

                //add X-WSSE header to HTTPWebRequest
                request.Headers.Add("X-WSSE", string.Join("", new string[] { "UsernameToken ", "Username=\"", credential.UserName, "\", ", "PasswordDigest=\"", digest, "\", ", "Nonce=\"", Convert.ToBase64String(nonce), "\", ", "Created=\"", createtime, "\"" }));

                //return new Authorization instance
                return new Authorization("WSSE profile=\"UsernameToken\"", true);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Find signature in challenge string.
        /// Challenge string may contain quoted strings.
        /// it should be elliminated when searching authentication signature
        /// </summary>
        private static bool ContainsSignatureInChallenge(string challenge, string signature)
        {
            int j = 0;
            int l = challenge.Length;
            while (true)
            {
                int i = challenge.IndexOf("\"");
                if (i < 0) { i = l; }
                if (challenge.Substring(j, i - j).Contains(signature)) return true;
                if ((i + 1) >= l) return false;
                j = challenge.IndexOf("\"", (int)(i + 1));
                if (j < 0) return false;
                if ((j + 1) >= l) return false;
            }
        }

        /// <summary>
        /// Create Secure Nonce
        /// </summary>
        private byte[] CreateNonce()
        {
            //make random octets
            byte[] buf = new byte[NonceLength];
            Rand.GetBytes(buf);
            return buf;
        }

    }
}
